﻿#region Copyright © 2009, DevLynx (MIT License)
// This software is licensed under The MIT License:
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// Algorithms and inspiration from:
//   http://channel9.msdn.com/playground/Sandbox/201158/
//   http://www.codeproject.com/KB/vista/textonglass.aspx
#endregion

//#define DevExpress

namespace AeroGlass
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Drawing;
    using System.Windows.Forms;
#if (DevExpress)
    using DevExpress.XtraEditors;
#endif // (DevExpress)

    /// <summary>
    /// Event fired when the Aero interface enabled is changed.
    /// </summary>
    public delegate void AeroEnabledChanged(Object sender, AeroEnabledChangedEventArgs e);

    /// <summary>
    /// Event fired when the Aero interface color is changed.
    /// </summary>
    public delegate void AeroColorChanged(Object sender, AeroColorChangedEventArgs e);

    /// <summary>
    /// AeroGlassProvider extends <see cref="System.Windows.Forms.Form"/>, <see cref="System.Windows.Forms.Label"/>,
    /// <see cref="System.Windows.Forms.PictureBox"/> and optionally, DevExpress.XtraEditors.LabelControl.
    /// AeroGlassProvider provides glass margins and manages drawing images and text onto the glass surface.
    /// </summary>
    [ProvideProperty("GlassMargins", typeof(Form))]
    [ProvideProperty("GlassTopMovesForm", typeof(Form))]
    [ProvideProperty("GlassEnabled", typeof(Form))]

    [ProvideProperty("GlassTextGlowSize", typeof(Control))]
    [ProvideProperty("IsRendered", typeof(Control))]
    [ProvideProperty("OnGlass", typeof(Control))]

    [ToolboxBitmap("AeroGlassProvider.bmp")]
    public partial class AeroGlassProvider : Component, IExtenderProvider
    {

        private bool disableBackColorChanged = false;
        private Dictionary<Control, AeroGlassControlProperties> glassControlProperties = new Dictionary<Control, AeroGlassControlProperties>();
        private Dictionary<Form, AeroGlassFormProperties> glassFormProperties = new Dictionary<Form, AeroGlassFormProperties>();
        private SortedDictionary<int, Control> zorder;


        /// <summary>
        /// Initializes a new instance of the <see cref="AeroGlassProvider"/> class.
        /// </summary>
        /// <param name="container">The container.</param>
        public AeroGlassProvider(IContainer container)
        {
            container.Add(this);

            InitializeComponent();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="AeroGlassProvider"/> class.
        /// </summary>
        public AeroGlassProvider()
        {
            InitializeComponent();
        }


        /// <summary>
        /// Gets a value indicating whether the Aero interface enabled.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this the Aero interface enabled; otherwise, <c>false</c>.
        /// </value>
        public static bool IsAeroEnabled
        {
            get { return AeroGlassUtility.IsAeroEnabled; }
        }



        /// <summary>
        /// Occurs when the Aero interface color has changed.
        /// </summary>
        public event AeroColorChanged AeroColorChanged;

        /// <summary>
        /// Occurs when the Aero interface enabled has changed.
        /// </summary>
        public event AeroEnabledChanged AeroEnabledChanged;


        /// <summary>
        /// Specifies whether this object can provide its extender properties to the specified object.
        /// </summary>
        /// <param name="extendee">The <see cref="T:System.Object"/> to receive the extender properties.</param>
        /// <returns>
        /// true if this object can provide extender properties to the specified object; otherwise, false.
        /// </returns>
        public bool CanExtend(object extendee)
        {
            bool result = false;
            if (extendee == this)
                return result;
            result = (extendee is Form) || (extendee is Label) || (extendee is PictureBox);
#if (DevExpress)
            result = result || (extendee is LabelControl);
#endif // (DevExpress)
            return result;
        }

        /// <summary>
        /// Gets the color of the Aero interface effect.
        /// </summary>
        /// <returns>The Aero interface color.</returns>
        public static Color GetAeroColor()
        {
            bool isOpaque;
            Color aeroColor;
            GetAeroColor(out aeroColor, out isOpaque);
            return aeroColor;
        }

        /// <summary>
        /// Gets whether glass should be extended into the client space.
        /// </summary>
        /// <param name="extendee">The <see cref="System.Windows.Forms.Form"/> to be extended.</param>
        /// <returns><c>true</c> if the glass is enabled; otherwise <c>false</c>.</returns>
        [DisplayName("GlassEnabled")]
        [DefaultValue(true)]
        [Category("Behavior")]
        [Description("Indicates whether extending glass into the client area is enabled.")]
        public bool GetGlassEnabled(Form extendee)
        {
            AeroGlassFormProperties properties = GetGlassProperties(extendee, false);
            if (properties == null)
                return true;
            return properties.GlassEnabled;
        }

        /// <summary>
        /// Gets the glass margins.
        /// </summary>
        /// <param name="extendee">The <see cref="System.Windows.Forms.Form"/> to be extended.</param>
        /// <returns>The margins where the glass will be extended.</returns>
        [DisplayName("GlassMargins")]
        [Description("Specifies the interior glass margin of the form. Set to -1 for full window glass.")]
        [Category("Layout")]
        public Padding GetGlassMargins(Form extendee)
        {
            AeroGlassFormProperties properties = GetGlassProperties(extendee, false);
            if (properties == null)
                return new Padding(0);
            return properties.GlassMargins;
        }

        /// <summary>
        /// Gets the size of the glow behind control text.
        /// </summary>
        /// <param name="extendee">The <see cref="System.Windows.Forms.Control"/> to be extended.</param>
        /// <returns>The size of glow.</returns>
        [DisplayName("GlassTextGlowSize")]
        [DefaultValue(AeroGlassControlProperties.DefaultGlassTextGlowSize)]
        [Category("Appearance")]
        [Description("Indicates the size of the glow behind control text.")]
        public int GetGlassTextGlowSize(Control extendee)
        {
            AeroGlassControlProperties properties = GetGlassControlProperties(extendee, false);
            if (properties == null)
                return AeroGlassControlProperties.DefaultGlassTextGlowSize;
            return properties.GlassTextGlowSize;
        }

        /// <summary>
        /// 	<para>Gets a value indicating whether clicking and dragging within the top margin will move the form.
        /// If this is set to true then the form must override it's WinProc and add:
        /// <code>aeroGlassProvider.WndProc(this, ref message);</code>.</para>
        /// </summary>
        /// <param name="extendee">The <see cref="System.Windows.Forms.Form"/> to be extended.</param>
        /// <returns><c>true</c> if clicking and dragging on the top margin moves the form; otherwise, <c>false</c>.</returns>
        [DisplayName("GlassTopMovesForm")]
        [DefaultValue(false)]
        [Category("Behavior")]
        [Description("Specifies if clicking and dragging within the top margin will move the form. "
            + "Must override forms WinProc and add: aeroGlassProvider.WndProc(this, ref message);")]
        public bool GetGlassTopMovesForm(Form extendee)
        {
            AeroGlassFormProperties properties = GetGlassProperties(extendee, false);
            if (properties == null)
                return false;
            return properties.GlassTopMovesForm;
        }

        /// <summary>
        /// Gets a value indicating whether the control is located on the glass.
        /// </summary>
        /// <param name="extendee">The <see cref="System.Windows.Forms.Control"/> to be extended.</param>
        /// <returns>
        /// 	<c>true</c> if the control is considered to be on the glass; otherwise, <c>false</c>.
        /// </returns>
        [DisplayName("OnGlass")]
        [DefaultValue(false)]
        [Category("Behavior")]
        [Description("Indicates whether this control is on the glass surface or not.")]
        public bool GetOnGlass(Control extendee)
        {
            AeroGlassControlProperties properties = GetGlassControlProperties(extendee, false);
            if (properties == null)
                return false;
            return properties.OnGlass;
        }

        /// <summary>
        /// Gets a value indicating whether the control should be rendered onto the glass surface.
        /// </summary>
        /// <param name="extendee">The <see cref="System.Windows.Forms.Control"/> to be extended.</param>
        /// <returns>
        /// 	<c>true</c> if the control is considered to be should be rendered onto the glass surface; otherwise, <c>false</c>.
        /// </returns>
        [DisplayName("IsRendered")]
        [DefaultValue(true)]
        [Category("Behavior")]
        [Description("Indicates whether this control should be rendered onto the glass surface.")]
        public bool GetIsRendered(Control extendee)
        {
            AeroGlassControlProperties properties = GetGlassControlProperties(extendee, false);
            if (properties == null)
                return false;
            return properties.IsRendered;
        }

        /// <summary>
        /// Invalidates the non glass client area. Call this methods if you need to invalidate 
        /// just the non-glass area of the form.
        /// </summary>
        /// <param name="form">The <see cref="System.Windows.Forms.Form"/> to be extended.</param>
        public void InvalidateNonGlassClientArea(Form form)
        {
            AeroGlassFormProperties properties = GetGlassProperties(form, false);
            if (properties == null)
                return;
            Padding glassMargin = properties.GlassMargins;
            Rectangle rect = new Rectangle(glassMargin.Left, glassMargin.Top, form.ClientRectangle.Width - glassMargin.Right,
                form.ClientRectangle.Height - glassMargin.Bottom);
            form.Invalidate(rect, false);
        }

        /// <summary>
        /// Determines whether the Aero interface color is opaque.
        /// </summary>
        /// <returns>
        /// 	<c>true</c> if the Aero interface color is opaque; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsAeroColorOpaque()
        {
            bool isOpaque;
            Color aeroColor;
            GetAeroColor(out aeroColor, out isOpaque);
            return isOpaque;
        }

        /// <summary>
        /// Resets the Z-Order for the form. If you change the Z-Order programmatically then
        /// you may want to reset the Z-Order in which the on glass controls are painted.
        /// </summary>
        /// <param name="form">The <see cref="System.Windows.Forms.Form"/> to be extended.</param>
        public void ResetZOrder(Form form)
        {
            FillZOrder(form, true);
        }

        /// <summary>
        /// Set whether the glass should be extended into the client space.
        /// </summary>
        /// <param name="extendee">The <see cref="System.Windows.Forms.Form"/> to be extended.</param>
        /// <param name="value">The enabled value.</param>
        public void SetGlassEnabled(Form extendee, bool value)
        {
            AeroGlassFormProperties properties = GetGlassProperties(extendee, true);
            properties.GlassEnabled = value;
            PaintGlass(extendee);
            PaintControls(extendee);
        }

        /// <summary>
        /// Sets the glass margins.
        /// </summary>
        /// <param name="extendee">The <see cref="System.Windows.Forms.Form"/> to be extended.</param>
        /// <param name="value">The margins where the glass will be extended.</param>
        public void SetGlassMargins(Form extendee, Padding value)
        {
            AeroGlassFormProperties properties = GetGlassProperties(extendee, true);
            if (value == null)
            {
                properties.GlassMargins = new Padding(0);
                extendee.Paint -= extendee_Paint;
                extendee.Resize -= extendee_Resize;
                extendee.ResizeEnd -= extendee_ResizeEnd;
                extendee.Shown -= extendee_Shown;
            }
            else
            {
                properties.GlassMargins = (Padding)value;
                extendee.Paint += extendee_Paint;
                if (!InDesignMode())
                {
                    extendee.Resize += extendee_Resize;
                    extendee.ResizeEnd += extendee_ResizeEnd;
                    extendee.Shown += extendee_Shown;
                }
            }
            extendee.Invalidate();
        }

        /// <summary>
        /// Sets the size of the glow behind control text.
        /// </summary>
        /// <param name="extendee">The <see cref="System.Windows.Forms.Control"/> to be extended.</param>
        /// <param name="value">The size of glow.</param>
        public void SetGlassTextGlowSize(Control extendee, int value)
        {
            AeroGlassControlProperties properties = GetGlassControlProperties(extendee, true);
            properties.GlassTextGlowSize = value;
        }

        /// <summary>
        /// 	<para>Sets a value indicating whether clicking and dragging within the top margin will move the form.
        /// If this is set to true then the form must override it's WinProc and add:
        /// <code>aeroGlassProvider.WndProc(this, ref message);</code>.</para>
        /// </summary>
        /// <param name="extendee">The <see cref="System.Windows.Forms.Form"/> to be extended.</param>
        /// <param name="value"><c>true</c> if clicking and dragging on the top margin moves the form; otherwise, <c>false</c>.</param>
        public void SetGlassTopMovesForm(Form extendee, bool value)
        {
            AeroGlassFormProperties properties = GetGlassProperties(extendee, true);
            properties.GlassTopMovesForm = value;
        }

        /// <summary>
        /// Sets a value indicating whether the control is located on the glass.
        /// </summary>
        /// <param name="extendee">The <see cref="System.Windows.Forms.Control"/> to be extended.</param>
        /// <param name="value"><c>true</c> if the control is considered to be on the glass; otherwise, <c>false</c></param>
        public void SetOnGlass(Control extendee, bool value)
        {
            AeroGlassControlProperties properties = GetGlassControlProperties(extendee, true);
            properties.OnGlass = value;
        }

        /// <summary>
        /// Sets a value indicating whether the control should be rendered onto the glass surface.
        /// </summary>
        /// <param name="extendee">The <see cref="System.Windows.Forms.Control"/> to be extended.</param>
        /// <param name="value"><c>true</c> if the control should be rendered onto the glass surface; otherwise, <c>false</c></param>
        public void SetIsRendered(Control extendee, bool value)
        {
            AeroGlassControlProperties properties = GetGlassControlProperties(extendee, true);
            properties.IsRendered = value;
        }

        /// <summary>
        /// Handles the AeroGlassProvider windows messages.
        /// </summary>
        /// <param name="form">The form being extended.</param>
        /// <param name="message">The windows message.</param>
        public void WndProc(Form form, ref Message message)
        {
            switch (message.Msg)
            {
                case InteropServices.WM_DWMCOMPOSITIONCHANGED:
                    HandleAeroEnabledChanged(form, message);
                    break;
                case InteropServices.WM_DWMCOLORIZATIONCOLORCHANGED:
                    HandleAeroColorChanged(form, message);
                    break;
                case InteropServices.WM_NCHITTEST:
                    MoveForm(form, ref message);
                    break;
            }
        }

        private void control_BackColorChanged(object sender, EventArgs e)
        {
            if (disableBackColorChanged)
                return;
            Control control = sender as Control;
            AeroGlassControlProperties properties = GetGlassControlProperties(control, false);
            if (properties != null)
            {
                properties.BackColor = control.BackColor;
            }
        }

        private void label_TextChanged(object sender, System.EventArgs e)
        {
            Control control = sender as Control;
            if (control != null)
            {
                Form form = control.FindForm();
                PaintControls(form, true);
            }
        }


        private void extendee_Paint(object sender, PaintEventArgs e)
        {
            PaintGlass(sender as Form, e.Graphics);
            PaintControls(sender as Form);
        }

        private void extendee_Resize(object sender, EventArgs e)
        {
            Form form = sender as Form;
            if (IsGlassEnabled(form) || InDesignMode())
            {
                InvalidateNonGlassClientArea(form);
            }
        }

        private void extendee_ResizeEnd(object sender, EventArgs e)
        {
            Form form = sender as Form;
            PaintControls(form, true);
        }

        private void extendee_Shown(object sender, EventArgs e)
        {
            PaintGlass(sender as Form);
            PaintControls(sender as Form);
        }

        private void FillZOrder(Form form)
        {
            FillZOrder(form, false);
        }

        private void FillZOrder(Form form, bool forceFill)
        {
            if (zorder == null)
            {
                zorder = new SortedDictionary<int, Control>(new zorderComparer());
                forceFill = true;
            }
            if (forceFill)
            {
                zorder.Clear();
                foreach (Control control in glassControlProperties.Keys)
                {
                    int index = form.Controls.GetChildIndex(control, false);
                    if (index >= 0)
                        zorder.Add(index, control);
                }
            }
        }

        private static void GetAeroColor(out Color aeroColor, out bool isOpaque)
        {
            Int32 aeroColorInt;
            AeroGlassUtility.GetAeroColor(out aeroColorInt, out isOpaque);
            aeroColor = Color.FromArgb(Convert.ToInt32(aeroColorInt));
        }

        private AeroGlassControlProperties GetGlassControlProperties(Control control, bool addIfNecessary)
        {
            AeroGlassControlProperties properties = null;
            if (!glassControlProperties.TryGetValue(control, out properties) && addIfNecessary)
            {
                properties = new AeroGlassControlProperties();
                properties.BackColor = control.BackColor;
                control.BackColorChanged += control_BackColorChanged;
                control.TextChanged += label_TextChanged;
                if (control is PictureBox)
                    (control as PictureBox).LoadCompleted += pictureBox_LoadCompleted;
                glassControlProperties.Add(control, properties);
            }
            return properties;
        }

        private void pictureBox_LoadCompleted(object sender, AsyncCompletedEventArgs e)
        {
            Control control = sender as Control;
            if (control != null)
            {
                Form form = control.FindForm();
                PaintControls(form, true);
            }
        }

        private AeroGlassFormProperties GetGlassProperties(Form form, bool addIfNecessary)
        {
            AeroGlassFormProperties properties = null;
            if (!glassFormProperties.TryGetValue(form, out properties) && addIfNecessary)
            {
                properties = new AeroGlassFormProperties();
                glassFormProperties.Add(form, properties);
            }
            return properties;
        }

        private bool HandleAeroColorChanged(Form form, Message message)
        {
            bool result = false;
            if (AeroGlassUtility.CompositionColorChanged(message))
            {
                result = true;
                if (AeroColorChanged != null)
                {
                    bool isOpaque;
                    Color aeroColor;
                    GetAeroColor(out aeroColor, out isOpaque);
                    AeroColorChanged(this, new AeroColorChangedEventArgs(aeroColor, isOpaque));
                }
            }
            return result;
        }

        private bool HandleAeroEnabledChanged(Form form, Message message)
        {
            bool result = false;
            if (AeroGlassUtility.CompositionChanged(message))
            {
                result = true;
                if (AeroEnabledChanged != null)
                    AeroEnabledChanged(this, new AeroEnabledChangedEventArgs(AeroGlassUtility.IsAeroEnabled));
            }
            return result;
        }

        private bool InDesignMode()
        {
            return DesignMode || Process.GetCurrentProcess().ProcessName == "devenv";
        }

        private bool IsGlassEnabled(Form form)
        {
            bool result;
            AeroGlassFormProperties properties = GetGlassProperties(form, false);
            if (properties == null)
                result = true;
            else
                result = properties.GlassEnabled;
            return result && AeroGlassUtility.IsAeroEnabled;
        }

        private bool MoveForm(Form form, ref Message message)
        {
            bool result = false;
            AeroGlassFormProperties properties = GetGlassProperties(form, false);
            if (properties == null || !IsGlassEnabled(form) || !properties.GlassTopMovesForm)
                result = false;
            else if (AeroGlassUtility.MouseIsOnClient(message))
            {
                Point screenCoordinates = form.PointToClient(AeroGlassUtility.ScreenCoordinates(message));
                // Check out if we are on the top glass margin.
                if (properties.IsFullFormGlass || properties.MarginRectangle(MarginSide.Top, form.ClientRectangle).Contains(screenCoordinates))
                    message.Result = AeroGlassUtility.CaptionIntPtr();
                result = true;
            }
            return result;
        }

        private void PaintControls(Form form)
        {
            PaintControls(form, false);
        }

        private void PaintControls(Form form, bool invalidateBackground)
        {
            if (form == null)
                return;

            if (!IsGlassEnabled(form))
                PaintControlsWithoutGlass();
            else if (!InDesignMode())
                PaintControlsOnGlass(form, invalidateBackground);
        }

        private void PaintControlsOnGlass(Form form, bool invalidateBackground)
        {
            // get the Z-Order and paint back to front.
            FillZOrder(form);

            foreach (Control control in zorder.Values)
            {
                AeroGlassControlProperties properties;
                if (glassControlProperties.TryGetValue(control, out properties))
                {
                    if (!properties.OnGlass)
                        continue;

                    disableBackColorChanged = true;
                    try
                    {
                        control.BackColor = Color.Black;
                        if (properties.IsRendered)
                        {
                            control.Visible = false;
                            if (invalidateBackground)
                                form.Invalidate(control.Bounds);
                            if (control is PictureBox)
                                AeroGlassUtility.DrawImageOnGlass(form, (control as PictureBox).Image, control.Bounds);
                            else if (control is Label)
                                AeroGlassUtility.DrawTextOnGlass(form, control.Text, control.Font, control.Bounds, properties.GlassTextGlowSize);
#if (DevExpress)
                            else if (control is LabelControl)
                                AeroGlassUtility.DrawTextOnGlass(form, control.Text, control.Font, control.Bounds, properties.GlassTextGlowSize);
#endif // (DevExpress)
                        }
                    }
                    finally
                    {
                        disableBackColorChanged = false;
                    }
                }
            }
        }

        private void PaintControlsWithoutGlass()
        {
            foreach (Control control in glassControlProperties.Keys)
            {
                AeroGlassControlProperties properties;
                if (glassControlProperties.TryGetValue(control, out properties))
                {
                    if (properties.OnGlass)
                    {
                        control.Visible = true;
                        disableBackColorChanged = true;
                        try
                        {
#if (DevExpress)
                            if (control is LabelControl)
                                (control as LabelControl).Appearance.BackColor = properties.BackColor;
#endif // (DevExpress)
                            control.BackColor = properties.BackColor;
                        }
                        finally
                        {
                            disableBackColorChanged = false;
                        }
                    }
                }
            }
        }

        private void PaintGlass(Form form)
        {
            PaintGlass(form, form.CreateGraphics());
        }

        private void PaintGlass(Form form, Graphics formGraphics)
        {
            if (!IsGlassEnabled(form) && !InDesignMode())
                return;

            AeroGlassFormProperties properties = GetGlassProperties(form, true);
            if (properties == null)
                return;

            // Paint the glass effect.
            using (Brush blackBrush = new SolidBrush(Color.Black))
            {
                if (properties.IsFullFormGlass)
                    formGraphics.FillRectangle(blackBrush, form.ClientRectangle);
                else
                    foreach (MarginSide side in new MarginSide[] { MarginSide.Top, MarginSide.Right, MarginSide.Left, MarginSide.Bottom })
                        formGraphics.FillRectangle(blackBrush, properties.MarginRectangle(side, form.ClientRectangle));
            }

            if (InDesignMode())
                return;

            // Paint the glass effect.
            AeroGlassUtility.PaintGlassEffect(form, properties.GlassMargins);
        }
    }

    internal class zorderComparer : IComparer<int>
    {

        public int Compare(int x, int y)
        {
            return y - x;
        }
    }
}
